iT邦幫忙

2022 iThome 鐵人賽

DAY 14
0

[Day14] Clojure Macro (1) 初探 Macro

不知不覺目前已經完賽兩週了,太感人了!

今天要講的是macro

跟Excel VBA那個macro 使用巨集錄製器自動化工作
是不一樣的喔!(no no no搖手指)

不負責任翻譯:macro!? 是什麼東西? (嚇到吃手手)

macro

之前在第三天提到,Clojure程式是由reader處理。
所謂的reader會一次讀取一個form,再算出form代表的東西。
例如以下算式的 * ,+

Ref:
https://livebook.manning.com/concept/clojure/form

另外一方面,
macro的作用就是可以用來調整form,先對form動手腳(調整data structure),然後才回去求值
這樣的好處是可以幫我們提供一些語法上的抽象化

找資料時很感謝網路上的大大畫了這張流程圖(Reference):

那我們趕快一邊來參考Clojure Threading Macros Guide,一邊定義自己的macro喔!
https://clojure.org/guides/threading_macros

定義自己的 macro

我們來定義一個macro可以把參數做轉換為字串並回傳:

 (defmacro simple-str [a] (str a))
=> #'tutorial.core/simple-str

不管什麼東西都 幫我來一份 加工變成字串吧!

這就是所謂的 對form動手腳

(simple-str [1 2 3 4])
=>"[1 2 3 4]"

(simple-str (1 2 3 4))
"(1 2 3 4)"

(simple-str (+ 1 2 +3 +4))
=> "(+ 1 2 3 4)"

保持原味最對味(直接拿到原本form的metadata.)

那我們不加工,保持原味最對味(想直接拿到原本form的metadata.),該怎麼做呢?

可以使用 &form&args 印出

(defmacro print-form [& args] ;
  (println &form))

用一些簡單的小例子來試試:

(print-form 123 :abc ["x" "y" "z"])
=> (print-form 123 :abc [x y z])
nil

(print-form [1 2 3 4 5])
=> (print-form [1 2 3 4 5])
nil

(print-form (+ 1 2))
=> (print-form (+ 1 2))
nil

macro裡的 &form vs. apply

(defmacro print-form [& args] 
  (println &form))

其實可以用

(defmacro print-form-with-apply [& args]
  (println (apply list 'print-form-with-apply args)))

改寫~

'print-form-with-apply 前面是 Quote (')的意思,可以得到完完整整的fn name!

(quote form)
=> form


(quote print-form-with-apply)
=> print-form-with-apply

來多學一個 apply API

apply 後面先接一個function,以及一到多個參數

(apply f args)(apply f x args)(apply f x y args)(apply f x y z args)
(apply f a b c d & args)
(defmacro print-form-with-apply [& args]
  (println (apply list 'print-form-with-apply args)))

=>
#'tutorial.core/print-form-with-apply


(print-form-with-apply (1 2 3))

;; 原本是什麼form我就連名(form)帶姓(fn name)的印出來

=>(print-form-with-apply (1 2 3))
nil

apply的簡單運用

假設我們想要回傳list裡最小的數字,可以拿給Numbers用的min來實作

舉個例子,接到某個function的回傳值是vector[50 100 150]

(min [50 100 150])
=> [50 100 150]

代表min(只)收到一組vector參數 [50 100 150]

但我們如果要拿vector裡的三個數字來比,加個apply就可以達到效果


(apply min [50 100 150])
;;(apply f a b c d & args)

=> 50

大家對apply有一點感覺了嗎?
apply分別拿出了vector內的element來比較min

apply的舉一反三時間(先不要)

然後我們就發現第九天Clojure Functional Programming與資料操作(2) - reduce
簡單的

(reduce + (range 1 101))` 

使用情境,

其實是可以用

(apply + (range 1 101))

來取代。

但去研究apply vs. reduce又會開啟另一個黑洞,還是先不要好了 ~

之所以會順手提到apply,是因為我們發現他跟

thread-first macro (->) vs thread-last macro (->>)

擺的位置分類實在是很接近,
所以就順便介紹新成員出場,讓大家增廣見聞一下~

明天就要正式進入到好多箭頭系列啦!敬請期待:)

as->

cond->
cond->>

some->
some->>

同場加映1:用 defmacro定義 unless

第11天在講Flow Control
提到if和if-not時,曾經聊到其他語言if-not有的有 unless語法

 (if-not (empty? []) :true :false)
=> :false

這種舉反例的想法是工程師的最愛(?)
趕快來用defmacro實做一個自己的unless

(defmacro unless [pred statement-a statement-b]
  `(if (not ~pred) ~statement-a ~statement-b))

前面的毛毛蟲~(Unquote) 剛好跟提到的 '(Quote) 相反,會把表達式變成值

Within a template, form will be treated as an expression to be replaced by its value.

定義好unless就來用用看囉!

 (if-not (empty? []) :true :false)
=> :false

(unless (empty? []) :true :false)
=> :false

 (unless false (println "OK Will do") (println "false is false"))
=> OK Will do
nil

(unless (not= 1 1) (println "1 + 1 equals 2") (println "false is false"))
1 + 1 equals 2
nil

同場加映2:用 macroexpand

macro是可以透過macroexpand 展開成原本的形式

例如

when是(if .. do ..)的macro(巨集),可以用 macroexpand來展開看原本的樣子:

(macroexpand '(when  (empty? []) true))
=>(if (empty? []) (do true))

->來破梗一下明天會介紹的單箭頭

(macroexpand '(-> 1 (+ 2) (* 3)))
=> (* (+ 1 2) 3)

(-> 1 (+ 2) (* 3)) 這樣運算式的順序是不是對人類眼睛比較好讀呢?(先加二再乘以三)

同場加映3:用 macroexpand-1

macroexpand-1會去判斷是否macro form,是的話回傳展開始,否則回傳normal form

If form represents a macro form, returns its expansion,
else returns form.

用上面的(先加二再乘以三)舉個例子,macroexpand-1 後接 'single quote

;; macro form return its expansion
 (macroexpand-1 '(-> 1 (+ 2) (* 3)))
(* (+ 1 2) 3)

;; form is not macro form, returns form
 (macroexpand-1 '(* (+ 1 2) 3))
(* (+ 1 2) 3)

咦?
那跟同場加映2的macroexpand的結果有什麼不同呢?

 (macroexpand '(-> 1 (+ 2) (* 3)))
=> (* (+ 1 2) 3)

其實沒有-1的,macroexpand就是macroexpand-1重複做很多次的意思啦~

Repeatedly calls macroexpand-1 on form 
until it no longer represents a macro form, then returns it.  

macroexpand-1 Syntax-quote

最後,稍微提一下

還有另一個東東叫做Syntax-quote,或是backquote
是給symbols和四大data collection(lists/ vectors/ sets/ maps) 之外用的

放一個clojure文件上的例子example reference:

; When testing macro expansion in a file instead of at the REPL, 
; please note that it may be necessary to use a backquote
; instead of a straight quote.

(defmacro iiinc [x]
  `(+ 3 ~x))

(deftest t-stuff
  ; This doesn't work.
  (println (macroexpand-1 '(iiinc 2))) ;=> (iiinc 2)

  ; Oddly, we can use the macro itself fine in our tests...
  (println (iiinc 2))    ;=> 5
  (is (= 5 (iiinc 2)))  ;=> unit test passes

  ; This fixes it by resolving the symbol iiinc at compile-time.
  (println (macroexpand-1 `(iiinc 2)))) ;=> (+ 3 2)

; Also, as the previous examples show, please remember that 
; you must quote the form you are providing to `macroexpand-1`.

最後的最後

macro的世界太多種quote了是否覺得心累呢?

來句Motivational Quotes激勵自己吧!

工程師的千里之行,始於足下:)


上一篇
[Day13] Clojure Flow Control (3) case / cond / doall
下一篇
[Day15] Clojure Macro (2) thread-first -> / thread-last ->>
系列文
後端Developer實戰ClojureScript: Reagent與前端框架 Reframe30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言